Skip to content

feat: add quadrant chart to compare page#2388

Open
graphieros wants to merge 39 commits intomainfrom
compare-quadrant-chart
Open

feat: add quadrant chart to compare page#2388
graphieros wants to merge 39 commits intomainfrom
compare-quadrant-chart

Conversation

@graphieros
Copy link
Copy Markdown
Contributor

@graphieros graphieros commented Apr 5, 2026

Resolves #2387

This adds a quadrant chart to the compare page, to visualise comparisons along 2 axes:

  • X: adoption
  • Y: package efficiency

The chart is displayed below the bar charts, and includes:

  • png, svg exports
  • alt text copy feature
Dark mode Light mode
dark mode light mode
  • A tooltip explains how the data is processed:
image
  • Hovering datapoints reveals facets ordered through both axes of the quadrant:
image

Data processing:

  • log scaled to handle large disparities: downloads, likes, install size, dependencies, package size
  • adoption score: mostly driven by downloads, with small contributions from freshness and npmx likes
  • efficiency score: based on install size, dependencies, vulnerabilities, TS support, deprecation

Some signals are inverted (so lower is better): size, deps, vulnerabilities
Deprecation is a hard override and forces min efficiency

The weights I have chosen can be subject to discussion, because of their arbitrary nature:

const WEIGHTS = {
  adoption: {
    downloads: 0.75, // dominant signal because they best reflect real-world adoption (in the data we have through facets currently)
    freshness: 0.15, // small correction so stale packages are slightly 
    likes: 0.1, // might be pumped up in the future when ./npmx likes are more mainstream
  },
  efficiency: {
    installSize: 0.3, // weighted highest because it best reflects consumer footprint

    // dependency weights are already measured in install size in some way, but still useful knobs to find the sweet spot
    dependencies: 0.05, // direct deps capture architectural and supply-chain complexity
    totalDependencies: 0.2, // same for total deps

    packageSize: 0.1,
    vulnerabilities: 0.2, // penalize security burden
    types: 0.2, // TS support
    // Note: the 'deprecated' metric is not weighed because it just forces a -1 evaluation
  },
}

/* Fixed logarithmic ceilings to normalize metrics onto a stable [-1, 1] scale.
*  This avoids dataset-relative min/max normalization, which would shift scores depending
*  on which packages are being compared. Ceilings act as reference points for what is
*  considered 'high' for each metric, ensuring consistent positioning across different
*  datasets while preserving meaningful differences via log scaling. 
*/
const LOG_CEILINGS = {
  downloads: 100_000_000,
  likes: 1000, // might be pumped up in the future when ./npmx likes are more mainstream
  installSize: 25_000_000,
  dependencies: 100,
  totalDependencies: 1_000,
  packageSize: 15_000_000,
}

const VULNERABILITY_PENALTY_MULTIPLIER = 2

Other

  • Bump vue-data-ui to 3.17.10 with updates for the quadrant chart component, and other fixes to prevent errors in tests related to Teleport when legends are disabled.

@graphieros graphieros linked an issue Apr 5, 2026 that may be closed by this pull request
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs.npmx.dev Ready Ready Preview, Comment Apr 6, 2026 8:28am
npmx.dev Ready Ready Preview, Comment Apr 6, 2026 8:28am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
npmx-lunaria Ignored Ignored Apr 6, 2026 8:28am

Request Review

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 5, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
i18n/locales/en.json Source changed, localizations will be marked as outdated.
i18n/locales/fr-FR.json Localization changed, will be marked as complete.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@graphieros graphieros marked this pull request as draft April 5, 2026 08:48
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a quadrant comparison chart to the Compare page: a new Vue <script setup> component FacetQuadrantChart.vue that maps incoming package data to a normalized quadrant dataset via createQuadrantDataset, computes adoption/efficiency scores (including freshness, vulnerability penalty, types/deprecation handling), assigns quadrants, and renders with VueUiQuadrant. Implements themed styling, responsive tooltips, PNG/SVG export handlers, alt-text generation/copy, print watermark sizing options, i18n entries/schema updates (EN/FR), a vue-data-ui dependency bump, and accompanying unit and a11y tests.

Possibly related PRs

Suggested labels

front, a11y

Suggested reviewers

  • danielroe
  • alexdln
  • ghostdevv
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR contains one minor out-of-scope change: FacetBarChart.vue colour adjustment from fgSubtle to fg, unrelated to quadrant chart feature. Consider separating the FacetBarChart.vue styling change into a separate PR or clarify its relationship to the quadrant chart feature.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the quadrant chart feature resolving issue #2387, with all core requirements met including chart display, exports, alt-text, and scoring methodology.
Description check ✅ Passed The pull request description clearly relates to the changeset, detailing the addition of a quadrant chart to the compare page with specific implementation details.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch compare-quadrant-chart

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 5, 2026

Codecov Report

❌ Patch coverage is 64.28571% with 75 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/components/Compare/FacetQuadrantChart.vue 66.33% 28 Missing and 6 partials ⚠️
app/utils/charts.ts 0.00% 22 Missing and 5 partials ⚠️
app/composables/useChartWatermark.ts 0.00% 8 Missing and 1 partial ⚠️
app/utils/compare-quadrant-chart.ts 93.15% 2 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: be4b4196-422e-437b-89ff-c11f23cf7b05

📥 Commits

Reviewing files that changed from the base of the PR and between 5324b96 and 11fd407.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • app/components/Compare/FacetQuadrantChart.vue
  • app/composables/useChartWatermark.ts
  • app/pages/compare.vue
  • app/utils/charts.ts
  • app/utils/compare-quadrant-chart.ts
  • i18n/locales/en.json
  • i18n/locales/fr-FR.json
  • i18n/schema.json
  • package.json
  • test/nuxt/a11y.spec.ts
  • test/unit/app/utils/compare-quadrant-chart.spec.ts

@graphieros graphieros marked this pull request as draft April 5, 2026 10:05
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/components/Compare/FacetQuadrantChart.vue (1)

136-183: Extract the tooltip renderer out of config.

This computed now mixes chart options, export wiring, and a large HTML template string. Pulling the tooltip formatter into a helper (or small dedicated component) would keep the config declarative and make the metric layout much easier to test.

As per coding guidelines "Keep functions focused and manageable (generally under 50 lines)".

Also applies to: 249-320


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d4c1378e-e4d8-49e2-9f38-20701313fbe0

📥 Commits

Reviewing files that changed from the base of the PR and between 901f8c2 and c27eb19.

📒 Files selected for processing (4)
  • app/components/Compare/FacetQuadrantChart.vue
  • app/utils/compare-quadrant-chart.ts
  • i18n/locales/en.json
  • i18n/locales/fr-FR.json
✅ Files skipped from review due to trivial changes (2)
  • i18n/locales/fr-FR.json
  • app/utils/compare-quadrant-chart.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • i18n/locales/en.json

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
app/components/Compare/FacetQuadrantChart.vue (2)

113-128: Consider avoiding object spread in map for slight efficiency gain.

Static analysis flags the spread operator in .map() as inefficient since it creates intermediate objects. For the typical small number of packages being compared, the impact is negligible, but if you wish to address the lint warning:

♻️ Suggested refactor to avoid spread
 const dataset = computed<VueUiQuadrantDatasetItem[]>(() => {
   return rawQuadrant.value.map((el: PackageQuadrantPoint) => {
-    return {
-      ...el,
-      fullname: el.name,
-      name: applyEllipsis(el.name, 20),
-      shape: 'circle',
-      color: isListedFramework(el.name) ? getFrameworkColor(el.name) : undefined,
-      series: [
-        {
-          name: applyEllipsis(el.name, 20),
-          x: el.x,
-          y: el.y,
-        },
-      ],
-    }
+    const item: VueUiQuadrantDatasetItem = {
+      id: el.id,
+      license: el.license,
+      name: applyEllipsis(el.name, 20),
+      fullname: el.name,
+      x: el.x,
+      y: el.y,
+      adoptionScore: el.adoptionScore,
+      efficiencyScore: el.efficiencyScore,
+      quadrant: el.quadrant,
+      metrics: el.metrics,
+      shape: 'circle',
+      color: isListedFramework(el.name) ? getFrameworkColor(el.name) : undefined,
+      series: [
+        {
+          name: applyEllipsis(el.name, 20),
+          x: el.x,
+          y: el.y,
+        },
+      ],
+    }
+    return item
   })
 })

287-316: Add fallback values for consistency and robustness.

Some metrics use ?? 0 fallbacks (lines 281, 285) while others don't. If datapoint?.category?.metrics is unexpectedly undefined, this could display "NaN%" or "undefined" in the tooltip.

♻️ Suggested fix to add consistent fallbacks
                 <div class="flex flex-row items-baseline gap-2">
                   <span class="text-fg-subtle">${$t('compare.quadrant_chart.label_freshness_score')}</span>
-                  <span class="text-fg text-sm">${Math.round(datapoint?.category?.metrics.freshnessPercent)}%</span>
+                  <span class="text-fg text-sm">${Math.round(datapoint?.category?.metrics?.freshnessPercent ?? 0)}%</span>
                 </div>

                 <div class="text-fg text-xs mt-4">${$t('compare.quadrant_chart.label_y_axis')}</div>
                 <div class="flex flex-row items-baseline gap-2">
                   <span class="text-fg-subtle">${$t('compare.facets.items.installSize.label')}</span>
-                  <span class="text-fg text-sm">${bytesFormatter.format(datapoint?.category?.metrics.installSize)}</span>
+                  <span class="text-fg text-sm">${bytesFormatter.format(datapoint?.category?.metrics?.installSize ?? 0)}</span>
                 </div>
                 <div class="flex flex-row items-baseline gap-2">
                   <span class="text-fg-subtle">${$t('compare.facets.items.packageSize.label')}</span>
-                  <span class="text-fg text-sm">${bytesFormatter.format(datapoint?.category?.metrics.packageSize)}</span>
+                  <span class="text-fg text-sm">${bytesFormatter.format(datapoint?.category?.metrics?.packageSize ?? 0)}</span>
                 </div>
                 <div class="flex flex-row items-baseline gap-2">
                   <span class="text-fg-subtle">${$t('compare.facets.items.dependencies.label')}</span>
-                  <span class="text-fg text-sm">${datapoint?.category?.metrics.dependencies}</span>
+                  <span class="text-fg text-sm">${datapoint?.category?.metrics?.dependencies ?? 0}</span>
                 </div>
                 <div class="flex flex-row items-baseline gap-2">
                   <span class="text-fg-subtle">${$t('compare.facets.items.totalDependencies.label')}</span>
-                  <span class="text-fg text-sm">${datapoint?.category?.metrics.totalDependencies}</span>
+                  <span class="text-fg text-sm">${datapoint?.category?.metrics?.totalDependencies ?? 0}</span>
                 </div>
                 <div class="flex flex-row items-baseline gap-2">
                   <span class="text-fg-subtle">${$t('compare.facets.items.vulnerabilities.label')}</span>
-                  <span class="text-fg text-sm">${datapoint?.category?.metrics.vulnerabilities}</span>
+                  <span class="text-fg text-sm">${datapoint?.category?.metrics?.vulnerabilities ?? 0}</span>
                 </div>

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 043b74b5-3b22-4b95-acb5-71e2dc39cb09

📥 Commits

Reviewing files that changed from the base of the PR and between c27eb19 and 2150f0a.

📒 Files selected for processing (4)
  • app/components/Compare/FacetQuadrantChart.vue
  • i18n/locales/en.json
  • i18n/locales/fr-FR.json
  • i18n/schema.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • i18n/locales/fr-FR.json
  • i18n/locales/en.json
  • i18n/schema.json

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/components/Compare/FacetQuadrantChart.vue (1)

113-128: Consider explicit property assignment to avoid spread in map.

The static analyser flags spreading inside .map() as inefficient. Since PackageQuadrantPoint has a known shape, you could destructure and reassign explicitly. However, if the utility may add properties in future that the chart relies on at runtime, keeping the spread preserves forward compatibility.

♻️ Optional: explicit property assignment
 const dataset = computed<VueUiQuadrantDatasetItem[]>(() => {
-  return rawQuadrant.value.map((el: PackageQuadrantPoint) => {
-    return {
-      ...el,
-      fullname: el.name,
-      name: applyEllipsis(el.name, 20),
-      shape: 'circle',
-      color: isListedFramework(el.name) ? getFrameworkColor(el.name) : undefined,
-      series: [
-        {
-          name: applyEllipsis(el.name, 20),
-          x: el.x,
-          y: el.y,
-        },
-      ],
-    }
+  return rawQuadrant.value.map((el: PackageQuadrantPoint) => ({
+    x: el.x,
+    y: el.y,
+    quadrant: el.quadrant,
+    category: el.category,
+    fullname: el.name,
+    name: applyEllipsis(el.name, 20),
+    shape: 'circle' as const,
+    color: isListedFramework(el.name) ? getFrameworkColor(el.name) : undefined,
+    series: [{ name: applyEllipsis(el.name, 20), x: el.x, y: el.y }],
+  }))
-  })
 })

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 35dff1bd-c07b-42e0-9c66-353be24e2794

📥 Commits

Reviewing files that changed from the base of the PR and between 2150f0a and 92e693f.

📒 Files selected for processing (1)
  • app/components/Compare/FacetQuadrantChart.vue

@serhalp serhalp added the needs review This PR is waiting for a review from a maintainer label Apr 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs review This PR is waiting for a review from a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

add quadrant chart on the compare page

4 participants